--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 8121de96d760c376582728fd547705ec14465f55
Parents : 09400ec
Author : Mark Qvist <mark@unsigned.io>
Date : 2025-11-09T13:58:40+01:00
Added simple call log to voice call screen
Changes
5 files changed, 266 insertions(+), 59 deletions(-)
Diff
diff --git a/sbapp/patches/PythonActivity.java b/sbapp/patches/PythonActivity.java
index 649a3028..41b9460f 100644
--- a/sbapp/patches/PythonActivity.java
+++ b/sbapp/patches/PythonActivity.java
@@ -60,7 +60,7 @@ public class PythonActivity extends SDLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
try { this.startIntent = getIntent(); }
- catch { Log.e(TAG, "Failed to get pending intent on activity create"); }
+ catch (Exception e) { Log.e(TAG, "Failed to get pending intent on activity create"); }
Log.v(TAG, "PythonActivity onCreate running");
resourceManager = new ResourceManager(this);
diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py
index 59cb1dab..38d7febc 100644
--- a/sbapp/sideband/core.py
+++ b/sbapp/sideband/core.py
@@ -2138,20 +2138,21 @@ class SidebandCore():
elif "get_service_log" in call: connection.send(self.get_service_log())
elif "start_voice" in call: connection.send(self.start_voice())
elif "stop_voice" in call: connection.send(self.stop_voice())
- elif "telephone_is_available" in call: connection.send(self.telephone.is_available) if self.telephone else False
- elif "telephone_is_in_call" in call: connection.send(self.telephone.is_in_call) if self.telephone else False
- elif "telephone_call_is_connecting" in call: connection.send(self.telephone.call_is_connecting) if self.telephone else False
- elif "telephone_is_ringing" in call: connection.send(self.telephone.is_ringing) if self.telephone else False
- elif "telephone_caller_info" in call: connection.send(self.telephone.caller.hash) if self.telephone and self.telephone.caller else None
- elif "telephone_set_busy" in call: connection.send(self.telephone.set_busy(call["telephone_set_busy"])) if self.telephone else False
- elif "telephone_dial" in call: connection.send(self.telephone.dial(call["telephone_dial"])) if self.telephone else False
- elif "telephone_hangup" in call: connection.send(self.telephone.hangup()) if self.telephone else False
- elif "telephone_answer" in call: connection.send(self.telephone.answer()) if self.telephone else False
- elif "telephone_set_speaker" in call: connection.send(self.telephone.set_speaker(call["telephone_set_speaker"])) if self.telephone else False
- elif "telephone_set_microphone" in call: connection.send(self.telephone.set_microphone(call["telephone_set_microphone"])) if self.telephone else False
- elif "telephone_set_ringer" in call: connection.send(self.telephone.set_ringer(call["telephone_set_ringer"])) if self.telephone else False
- elif "telephone_set_low_latency_output" in call: connection.send(self.telephone.set_low_latency_output(call["telephone_set_low_latency_output"])) if self.telephone else False
- elif "telephone_announce" in call: connection.send(self.telephone.announce()) if self.telephone else False
+ elif "telephone_is_available" in call: connection.send(self.telephone.is_available if self.telephone else False)
+ elif "telephone_is_in_call" in call: connection.send(self.telephone.is_in_call if self.telephone else False)
+ elif "telephone_call_is_connecting" in call: connection.send(self.telephone.call_is_connecting if self.telephone else False)
+ elif "telephone_is_ringing" in call: connection.send(self.telephone.is_ringing if self.telephone else False)
+ elif "telephone_caller_info" in call: connection.send(self.telephone.caller.hash if self.telephone and self.telephone.caller else None)
+ elif "telephone_set_busy" in call: connection.send(self.telephone.set_busy(call["telephone_set_busy"]) if self.telephone else False)
+ elif "telephone_dial" in call: connection.send(self.telephone.dial(call["telephone_dial"]) if self.telephone else False)
+ elif "telephone_hangup" in call: connection.send(self.telephone.hangup() if self.telephone else False)
+ elif "telephone_answer" in call: connection.send(self.telephone.answer() if self.telephone else False)
+ elif "telephone_set_speaker" in call: connection.send(self.telephone.set_speaker(call["telephone_set_speaker"]) if self.telephone else False)
+ elif "telephone_set_microphone" in call: connection.send(self.telephone.set_microphone(call["telephone_set_microphone"]) if self.telephone else False)
+ elif "telephone_set_ringer" in call: connection.send(self.telephone.set_ringer(call["telephone_set_ringer"]) if self.telephone else False)
+ elif "telephone_set_low_latency_output" in call: connection.send(self.telephone.set_low_latency_output(call["telephone_set_low_latency_output"]) if self.telephone else False)
+ elif "telephone_announce" in call: connection.send(self.telephone.announce() if self.telephone else False)
+ elif "telephone_get_call_log" in call: connection.send(self.telephone.get_call_log() if self.telephone else [])
else:
connection.send(None)
@@ -5518,9 +5519,10 @@ class SidebandCore():
RNS.log("Starting voice service", RNS.LOG_DEBUG)
self.voice_running = True
self.setstate("voice.running", self.voice_running)
- from .voice import ReticulumTelephone
- self.telephone = ReticulumTelephone(self.identity, owner=self, speaker=self.config["voice_output"], microphone=self.config["voice_input"], ringer=self.config["voice_ringer"])
ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus")
+ call_log_path = os.path.join(self.app_dir, "app_storage", "lxst_call_log")
+ from .voice import ReticulumTelephone
+ self.telephone = ReticulumTelephone(self.identity, owner=self, speaker=self.config["voice_output"], microphone=self.config["voice_input"], ringer=self.config["voice_ringer"], logpath=call_log_path)
self.telephone.set_ringtone(ringtone_path)
self.telephone.set_low_latency_output(self.config["voice_low_latency"])
return True
@@ -5584,6 +5586,13 @@ class SidebandCore():
if self.gui_foreground(): RNS.log("Squelching call notification since GUI is in foreground", RNS.LOG_DEBUG)
else: self.notify(title="Incoming voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
+ def missed_call(self, remote_identity):
+ display_name = self.voice_display_name(remote_identity.hash)
+ self.setstate("voice.incoming_call", display_name)
+ # if self.gui_foreground(): RNS.log("Squelching call notification since GUI is in foreground", RNS.LOG_DEBUG)
+ # else: self.notify(title="Missed voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
+ self.notify(title="Missed voice call", content=f"From {display_name}", group="LXST.Telephony", context_id="incoming_call")
+
rns_config = """# This template is used to generate a
# running configuration for Sideband's
# internal RNS instance. Incorrect changes
diff --git a/sbapp/sideband/voice.py b/sbapp/sideband/voice.py
index 72ad879d..f40f1355 100644
--- a/sbapp/sideband/voice.py
+++ b/sbapp/sideband/voice.py
@@ -6,6 +6,7 @@ import time
from LXST._version import __version__
from LXST.Primitives.Telephony import Telephone
from RNS.vendor.configobj import ConfigObj
+import RNS.vendor.umsgpack as msgpack
class ReticulumTelephone():
STATE_AVAILABLE = 0x00
@@ -22,7 +23,9 @@ class ReticulumTelephone():
WAIT_TIME = 60
PATH_TIME = 10
- def __init__(self, identity, owner = None, service = False, speaker=None, microphone=None, ringer=None):
+ CALL_LOG_KEEP = 30*24*60*60
+
+ def __init__(self, identity, owner = None, service = False, speaker=None, microphone=None, ringer=None, logpath=None):
self.identity = identity
self.service = service
self.owner = owner
@@ -40,6 +43,8 @@ class ReticulumTelephone():
self.speaker_device = speaker
self.microphone_device = microphone
self.ringer_device = ringer
+ self.logpath = logpath
+ self.call_log = None
self.phonebook = {}
self.aliases = {}
self.names = {}
@@ -113,6 +118,54 @@ class ReticulumTelephone():
def set_busy(self, busy): self.telephone.set_busy(busy)
def set_low_latency_output(self, enabled): self.telephone.set_low_latency_output(enabled)
+ def get_call_log(self):
+ if self.call_log: return self.call_log
+ else:
+ call_log = []
+ try:
+ if os.path.isfile(self.logpath):
+ with open(self.logpath, "rb") as logfile:
+ read_call_log = msgpack.unpackb(logfile.read())
+
+ for entry in read_call_log:
+ age = time.time()-entry["time"]
+ if age < self.CALL_LOG_KEEP: call_log.append(entry)
+
+ except Exception as e: RNS.log(f"Could not read call log file: {e}", RNS.LOG_ERROR)
+ self.call_log = call_log
+ return self.call_log
+
+ def log_call(self, event, identity):
+ RNS.log(f"Logging call event {event} for {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG)
+ if self.logpath:
+ try:
+ if not os.path.isfile(self.logpath):
+ try:
+ with open(self.logpath, "wb") as logfile: logfile.write(msgpack.packb([]))
+ except Exception as e: raise OSError("Could not create call log file")
+
+ call_log = []
+ read_call_log = []
+ try:
+ with open(self.logpath, "rb") as logfile: read_call_log = msgpack.unpackb(logfile.read())
+ except Exception as e:
+ RNS.log(f"Error while reading call log file: {e}", RNS.LOG_ERROR)
+ RNS.log(f"Call log file will be re-created", RNS.LOG_ERROR)
+
+ for entry in read_call_log:
+ age = time.time()-entry["time"]
+ if age < self.CALL_LOG_KEEP: call_log.append(entry)
+
+ entry = {"time": time.time(), "event": event, "identity": identity.hash}
+ call_log.append(entry)
+
+ with open(self.logpath, "wb") as logfile: logfile.write(msgpack.packb(call_log))
+ self.call_log = call_log
+
+ except Exception as e:
+ RNS.log(f"An error occurred while updating call log: {e}", RNS.LOG_ERROR)
+ RNS.trace_exception(e)
+
def dial(self, identity_hash):
self.last_dialled_identity_hash = identity_hash
destination_hash = RNS.Destination.hash_from_name_and_identity("lxst.telephony", identity_hash)
@@ -145,6 +198,10 @@ class ReticulumTelephone():
self.owner.incoming_call(remote_identity)
def call_ended(self, remote_identity):
+ call_was_connecting = self.call_is_connecting
+ was_ringing = self.is_ringing
+ was_in_call = self.is_in_call
+
if self.is_in_call or self.is_ringing or self.call_is_connecting:
if self.is_in_call: RNS.log(f"Call with {RNS.prettyhexrep(self.caller.hash)} ended\n", RNS.LOG_DEBUG)
if self.is_ringing: RNS.log(f"Call {self.direction} {RNS.prettyhexrep(self.caller.hash)} was not answered\n", RNS.LOG_DEBUG)
@@ -152,11 +209,23 @@ class ReticulumTelephone():
self.direction = None
self.state = self.STATE_AVAILABLE
+ if call_was_connecting: self.log_call("outgoing-failure", remote_identity)
+ elif was_in_call: self.log_call("ongoing-ended", remote_identity)
+ elif was_ringing:
+ self.log_call("incoming-missed", remote_identity)
+ self.owner.missed_call(remote_identity)
+
def call_established(self, remote_identity):
+ call_was_connecting = self.call_is_connecting
+ was_ringing = self.is_ringing
+
if self.call_is_connecting or self.is_ringing:
self.state = self.STATE_IN_CALL
RNS.log(f"Call established with {RNS.prettyhexrep(self.caller.hash)}", RNS.LOG_DEBUG)
+ if call_was_connecting: self.log_call("outgoing-success", remote_identity)
+ elif was_ringing: self.log_call("incoming-success", remote_identity)
+
def __is_allowed(self, identity_hash):
if self.owner.config["voice_trusted_only"]:
return self.owner.voice_is_trusted(identity_hash)
@@ -202,4 +271,5 @@ class ReticulumTelephoneProxy():
def set_microphone(self, microphone): return self.owner.service_rpc_request({"telephone_set_microphone": microphone })
def set_ringer(self, ringer): return self.owner.service_rpc_request({"telephone_set_ringer": ringer })
def set_low_latency_output(self, enabled): return self.owner.service_rpc_request({"telephone_set_low_latency_output": enabled})
- def announce(self): return self.owner.service_rpc_request({"telephone_announce": True})
\ No newline at end of file
+ def announce(self): return self.owner.service_rpc_request({"telephone_announce": True})
+ def get_call_log(self): return self.owner.service_rpc_request({"telephone_get_call_log": True})
\ No newline at end of file
diff --git a/sbapp/ui/helpers.py b/sbapp/ui/helpers.py
index 5b432788..d8c0aaa5 100644
--- a/sbapp/ui/helpers.py
+++ b/sbapp/ui/helpers.py
@@ -6,6 +6,7 @@ from kivymd.uix.list import OneLineIconListItem, MDList, IconLeftWidget, IconRig
from kivy.properties import StringProperty
import re
+ts_format_date = "%Y-%m-%d"
ts_format = "%Y-%m-%d %H:%M:%S"
file_ts_format = "%Y_%m_%d_%H_%M_%S"
diff --git a/sbapp/ui/voice.py b/sbapp/ui/voice.py
index b0f8e9aa..ff604bcb 100644
--- a/sbapp/ui/voice.py
+++ b/sbapp/ui/voice.py
@@ -12,6 +12,8 @@ from kivymd.uix.pickers import MDColorPicker
from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.button import MDRectangleFlatIconButton
from kivymd.uix.dialog import MDDialog
+from kivy.properties import StringProperty, BooleanProperty, OptionProperty, ColorProperty, Property
+from kivymd.uix.list import MDList, IconLeftWidget, IconRightWidget, OneLineAvatarIconListItem
from kivymd.icon_definitions import md_icons
from kivymd.toast import toast
from kivy.properties import StringProperty, BooleanProperty
@@ -22,10 +24,16 @@ import threading
from datetime import datetime
if RNS.vendor.platformutils.get_platform() == "android":
- from ui.helpers import ts_format
+ from ui.helpers import ts_format_date
from android.permissions import request_permissions, check_permission
else:
- from .helpers import ts_format
+ from .helpers import ts_format_date
+
+from kivy.utils import escape_markup
+if RNS.vendor.platformutils.get_platform() == "android":
+ from ui.helpers import multilingual_markup
+else:
+ from .helpers import multilingual_markup
class Voice():
def __init__(self, app):
@@ -37,6 +45,9 @@ class Voice():
self.path_requesting = None
self.output_devices = []
self.input_devices = []
+ self.log_list = None
+ self.last_log_update = 0
+ self.log_name_cache = {}
self.listed_output_devices = []
self.listed_input_devices = []
self.listed_ringer_devices = []
@@ -46,9 +57,8 @@ class Voice():
self.screen.app = self.app
self.screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.screen)
+ self.update_call_log()
- self.screen.ids.voice_scrollview.effect_cls = ScrollEffect
-
def update_call_status(self, dt=None):
if self.app.root.ids.screen_manager.current == "voice_screen":
if self.ui_updater == None: self.ui_updater = Clock.schedule_interval(self.update_call_status, 0.5)
@@ -89,6 +99,8 @@ class Voice():
db.disabled = True; db.text = "Voice calls disabled"
ih.disabled = True
+ if time.time() > self.last_log_update+3: self.update_call_log()
+
def target_valid(self):
if self.app.sideband.voice_running:
db = self.screen.ids.dial_button
@@ -170,7 +182,7 @@ class Voice():
self.update_call_status()
- ### settings screen
+ ### Settings screen
######################################
def settings_action(self, sender=None):
@@ -297,6 +309,116 @@ class Voice():
self.app.sideband.telephone.set_ringer(self.app.sideband.config["voice_ringer"])
+ ### Call log
+ ######################################
+
+ def update_call_log(self):
+ if self.log_list == None:
+ self.log_list = CallList()
+ self.screen.ids.log_list_container.add_widget(self.log_list)
+
+ self.update_log_list()
+ self.last_log_update = time.time()
+
+ def update_log_list(self):
+ call_log = self.app.sideband.telephone.get_call_log()
+ call_log.sort(key=lambda e: e["time"], reverse=True)
+ data = []
+ for entry in call_log:
+ try:
+ at = entry["time"]
+ td = int(time.time())-int(at)
+ evt = entry["event"]
+ idnt = entry["identity"]
+
+ if not idnt in self.log_name_cache: self.log_name_cache[idnt] = self.app.sideband.voice_display_name(idnt)
+ name = multilingual_markup(escape_markup(str(self.log_name_cache[idnt])).encode("utf-8")).decode("utf-8")
+
+ icon = None
+ if evt == "incoming-missed": icon = "phone-missed"
+ elif evt == "outgoing-failure": icon = "phone-cancel"
+ elif evt == "incoming-success": icon = "phone-incoming"
+ elif evt == "outgoing-success": icon = "phone-outgoing"
+
+ time_str = None
+ if td < 60: time_str = "Just now"
+ elif td < 60*60: td = int((td//60)*60)
+ elif td < 60*60*24: td = int((td//60)*60)
+ elif td < 60*60*24*7: td = int((td//(60*60*24))*(60*60*24))
+ else: time_str = time.strftime(ts_format_date, time.localtime(at))
+
+ if time_str == None: time_str = f"{RNS.prettytime(td)} ago"
+
+ if icon:
+ info = f"{name} • [i]{time_str}[/i]"
+ entry = {"icon": icon, "text": f"{info}"}
+ data.append(entry)
+
+ except Exception as e:
+ RNS.log(f"An error occurred while updating the call log list: {e}", RNS.LOG_ERROR)
+ RNS.trace_exception(e)
+
+ self.log_list.data = data
+
+class LogEntry(OneLineAvatarIconListItem):
+ app = None
+ owner_screen = None
+ conversation_dropdown = None
+ voice_dropdown = None
+ clear_dialog = None
+ clear_telemetry_dialog = None
+ delete_dialog = None
+
+ icon = StringProperty()
+ # ti_color = OptionProperty(None, options=theme_text_color_options)
+ # icon_fg = Property(None, allownone=True)
+ # icon_bg = Property(None, allownone=True)
+
+ def __init__(self):
+ super().__init__()
+ # self.bind(on_release=self.app.conversation_action)
+ # self.ids.left_icon.bind(on_release=self.left_icon_action)
+ # self.ids.right_icon.bind(on_release=self.right_icon_action)
+
+ def left_icon_action(self, sender):
+ pass
+
+ def right_icon_action(self, sender):
+ pass
+
+class CallList(MDRecycleView):
+ def __init__(self):
+ super().__init__()
+ self.data = []
+
+Builder.load_string("""
+<LogEntry>
+ IconLeftWidget:
+ id: left_icon
+ # theme_icon_color: root.ti_color
+ # icon_color: root.icon_fg
+ # md_bg_color: root.icon_bg
+ icon: root.icon
+ _default_icon_pad: dp(14)
+ icon_size: dp(24)
+
+ # IconRightWidget:
+ # id: right_icon
+ # icon: "dots-vertical"
+
+<CallList>:
+ id: calls_scrollview
+ viewclass: "LogEntry"
+ effect_cls: "ScrollEffect"
+
+ RecycleBoxLayout:
+ default_size: None, dp(57)
+ default_size_hint: 1, None
+ size_hint_y: None
+ height: self.minimum_height
+ orientation: "vertical"
+""")
+
layout_voice_screen = """
MDScreen:
name: "voice_screen"
@@ -316,48 +438,53 @@ MDScreen:
['close', lambda x: root.app.close_any_action(self)],
]
- ScrollView:
- id: voice_scrollview
+ MDBoxLayout:
+ orientation: "vertical"
+ size_hint_y: None
+ height: self.minimum_height
+ padding: [dp(28), dp(32), dp(28), dp(16)]
MDBoxLayout:
orientation: "vertical"
+ # spacing: "24dp"
size_hint_y: None
height: self.minimum_height
- padding: [dp(28), dp(32), dp(28), dp(16)]
+ padding: [dp(0), dp(12), dp(0), dp(0)]
- MDBoxLayout:
- orientation: "vertical"
- # spacing: "24dp"
- size_hint_y: None
- height: self.minimum_height
- padding: [dp(0), dp(12), dp(0), dp(0)]
+ MDTextField:
+ id: identity_hash
+ hint_text: "Identity hash"
+ mode: "rectangle"
+ # size_hint: [1.0, None]
+ pos_hint: {"center_x": .5}
+ max_text_length: 32
+ on_text: root.delegate.target_input_action(self)
- MDTextField:
- id: identity_hash
- hint_text: "Identity hash"
- mode: "rectangle"
- # size_hint: [1.0, None]
- pos_hint: {"center_x": .5}
- max_text_length: 32
- on_text: root.delegate.target_input_action(self)
-
- MDBoxLayout:
- orientation: "vertical"
- spacing: "24dp"
- size_hint_y: None
- height: self.minimum_height
- padding: [dp(0), dp(35), dp(0), dp(35)]
-
- MDRectangleFlatIconButton:
- id: dial_button
- icon: "phone-outgoing"
- text: "Call"
- padding: [dp(0), dp(14), dp(0), dp(14)]
- icon_size: dp(24)
- font_size: dp(16)
- size_hint: [1.0, None]
- on_release: root.delegate.dial_action(self)
- disabled: True
+ MDBoxLayout:
+ orientation: "vertical"
+ spacing: "24dp"
+ size_hint_y: None
+ height: self.minimum_height
+ padding: [dp(0), dp(35), dp(0), dp(35)]
+
+ MDRectangleFlatIconButton:
+ id: dial_button
+ icon: "phone-outgoing"
+ text: "Call"
+ padding: [dp(0), dp(14), dp(0), dp(14)]
+ icon_size: dp(24)
+ font_size: dp(16)
+ size_hint: [1.0, None]
+ on_release: root.delegate.dial_action(self)
+ disabled: True
+
+ MDSeparator:
+ orientation: "horizontal"
+ height: dp(1)
+
+ MDBoxLayout:
+ orientation: "vertical"
+ id: log_list_container
"""
layout_voice_settings_screen = """
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────